feat(init): add fuzzy search for module selection#1180
Conversation
commit: |
📦 Bundle Size Comparison📈 nuxi
📈 nuxt-cli
📈 create-nuxt
|
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #1180 +/- ##
=======================================
Coverage ? 25.19%
=======================================
Files ? 90
Lines ? 4910
Branches ? 279
=======================================
Hits ? 1237
Misses ? 3643
Partials ? 30 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
CodSpeed Performance ReportMerging this PR will not alter performanceComparing Summary
|
abedffb to
ebcafc2
Compare
📝 WalkthroughWalkthroughAdds an interactive fuzzy-search module selector and integrates it into the CLI. New file packages/nuxi/src/commands/module/_autocomplete.ts exports AutocompleteOptions, AutocompleteResult, and selectModulesAutocomplete which uses fzf + clack prompts. init and module add commands were updated to use the new autocomplete flow. Unit tests for the autocomplete feature were added at packages/nuxi/test/unit/commands/module/_autocomplete.spec.ts. The runtime dependency "fzf" was added to packages/nuxi and packages/nuxt-cli package.json, and knip.json was updated to ignore fzf for the nuxt-cli workspace. Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
packages/nuxi/src/commands/init.ts (1)
455-484:⚠️ Potential issue | 🟡 MinorRespect explicit cancellation from module autocomplete.
If the user cancels the autocomplete prompt, the flow currently proceeds silently. Consider handling
result.cancelledto abort consistently with other prompts.💡 Suggested adjustment
- const result = await selectModulesAutocomplete({ modules: allModules }) + const result = await selectModulesAutocomplete({ modules: allModules }) + if (result.cancelled) { + cancel('Operation cancelled.') + process.exit(1) + } if (result.selected.length > 0) { const modules = result.selected
🤖 Fix all issues with AI agents
In `@packages/nuxi/package.json`:
- Line 54: The package.json currently lists "fzf" under devDependencies but it
is imported/used at runtime in
packages/nuxi/src/commands/module/_autocomplete.ts, so move the "fzf" entry from
devDependencies to dependencies in packages/nuxi/package.json (i.e., remove it
from devDependencies and add the same version under dependencies) and then
reinstall/build so the runtime bundle includes it.
In `@packages/nuxi/src/commands/module/add.ts`:
- Around line 88-115: The interactive branch can leave the spinner running on
fetch failures and doesn't handle explicit cancellation from
selectModulesAutocomplete; wrap the Promise.all call that uses fetchModules()
and getNuxtVersion(cwd) in a try/catch (or use .catch) and ensure
spinner().stop() is called in a finally block so the spinner always stops on
error, logging or calling cancel() with the caught error and exiting; after
calling selectModulesAutocomplete({modules: compatibleModules,...}) check
result.cancelled first and call cancel('Selection cancelled.') and exit, then
handle result.selected.length === 0 as the empty-selection case, and keep
references to spinner, fetchModules, getNuxtVersion, selectModulesAutocomplete,
cancel and process.exit when implementing these guards.
🧹 Nitpick comments (1)
packages/nuxi/src/commands/module/_autocomplete.ts (1)
41-61: Cache fuzzy results per search term to avoid O(n²) filtering.
fzf.find(search)is recomputed for each option; a cached Set per search term cuts repeated work on large module lists.♻️ Proposed refactor
const fzf = new Fzf(sortedModules, { selector: m => `${m.npm} ${m.name} ${m.category}`, casing: 'case-insensitive', tiebreakers: [byLengthAsc], }) + const matchCache = new Map<string, Set<string>>() // Custom filter function using fzf for fuzzy matching const filter = (search: string, option: Option<string>): boolean => { if (!search) return true - const results = fzf.find(search) - return results.some(r => r.item.npm === option.value) + let matches = matchCache.get(search) + if (!matches) { + matches = new Set(fzf.find(search).map(r => r.item.npm)) + matchCache.set(search, matches) + } + return matches.has(option.value) }
| "defu": "^6.1.4", | ||
| "exsolve": "^1.0.8", | ||
| "fuse.js": "^7.1.0", | ||
| "fzf": "^0.5.2", |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# Check if fzf is imported in the mentioned file
cat packages/nuxi/src/commands/module/_autocomplete.ts | head -50Repository: nuxt/cli
Length of output: 1582
🏁 Script executed:
# Check the build configuration and package.json structure
cat packages/nuxi/package.json | head -80Repository: nuxt/cli
Length of output: 2002
🏁 Script executed:
# Search for fzf usage in the codebase
rg "fzf" packages/nuxi/src --type ts --type jsRepository: nuxt/cli
Length of output: 515
🏁 Script executed:
# Check for build configuration files
fd -e "tsconfig\|vite\|webpack\|rollup\|build" packages/nuxi/ --type fRepository: nuxt/cli
Length of output: 413
🏁 Script executed:
# Search for tsdown configuration files
fd -e config -name "*tsdown*" packages/nuxi/
fd . packages/nuxi --maxdepth 2 --type f \( -name "tsdown*" -o -name "build*" -o -name ".tsdownrc*" \)Repository: nuxt/cli
Length of output: 513
🏁 Script executed:
# Check if there's a dist folder and what it contains
ls -la packages/nuxi/dist 2>/dev/null | head -20 || echo "dist not found or not built"Repository: nuxt/cli
Length of output: 34
🏁 Script executed:
# Search for external/externalize config in package.json or tsconfig
grep -i "external\|bundle" packages/nuxi/package.jsonRepository: nuxt/cli
Length of output: 34
🏁 Script executed:
# Check tsdown documentation/behavior in context of how packages are typically bundled
rg "tsdown|bundle" packages/nuxi --type json --type ts -A 2 | head -30Repository: nuxt/cli
Length of output: 1693
🏁 Script executed:
# Read the tsdown configuration
cat -n packages/nuxi/tsdown.config.tsRepository: nuxt/cli
Length of output: 892
🏁 Script executed:
# Also check if there's any external configuration in the config
cat packages/nuxi/tsdown.config.ts | grep -i "external\|dependencies"Repository: nuxt/cli
Length of output: 34
Move fzf from devDependencies to dependencies.
fzf is imported and used at runtime in the CLI (packages/nuxi/src/commands/module/_autocomplete.ts). The tsdown build configuration does not bundle dependencies by default, meaning consumers who install the package will not receive fzf since it's currently only a devDependency. This will cause the CLI to fail at runtime with a "module not found" error when the autocomplete functionality is invoked.
Proposed fix
{
"name": "nuxi",
"type": "module",
"version": "3.32.0",
"description": "Nuxt CLI",
"license": "MIT",
"repository": {
"type": "git",
"url": "git+https://github.com/nuxt/cli.git",
"directory": "packages/nuxi"
},
"exports": {
".": "./dist/index.mjs",
"./cli": "./bin/nuxi.mjs"
},
"types": "./dist/index.d.ts",
"bin": {
"nuxi": "bin/nuxi.mjs",
"nuxi-ng": "bin/nuxi.mjs",
"nuxt": "bin/nuxi.mjs",
"nuxt-cli": "bin/nuxi.mjs"
},
"files": [
"bin",
"dist"
],
"engines": {
"node": "^16.10.0 || >=18.0.0"
},
"scripts": {
"build": "tsdown",
"prepack": "pnpm build",
"test:dist": "node ./bin/nuxi.mjs info ../../playground"
},
+ "dependencies": {
+ "fzf": "^0.5.2"
+ },
"devDependencies": {
"@bomb.sh/tab": "^0.0.12",
"@clack/prompts": "1.0.0",
"@nuxt/kit": "^4.3.0",
"@nuxt/schema": "^4.3.0",
"@nuxt/test-utils": "^3.23.0",
"@types/copy-paste": "^2.1.0",
"@types/debug": "^4.1.12",
"@types/node": "^24.10.10",
"@types/semver": "^7.7.1",
"c12": "^3.3.3",
"citty": "^0.2.0",
"confbox": "^0.2.2",
"consola": "^3.4.2",
"copy-paste": "^2.2.0",
"debug": "^4.4.3",
"defu": "^6.1.4",
"exsolve": "^1.0.8",
"fuse.js": "^7.1.0",
- "fzf": "^0.5.2",
"giget": "^3.1.1",📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| "fzf": "^0.5.2", | |
| "scripts": { | |
| "build": "tsdown", | |
| "prepack": "pnpm build", | |
| "test:dist": "node ./bin/nuxi.mjs info ../../playground" | |
| }, | |
| "dependencies": { | |
| "fzf": "^0.5.2" | |
| }, | |
| "devDependencies": { | |
| "@bomb.sh/tab": "^0.0.12", | |
| "@clack/prompts": "1.0.0", | |
| "@nuxt/kit": "^4.3.0", | |
| "@nuxt/schema": "^4.3.0", | |
| "@nuxt/test-utils": "^3.23.0", | |
| "@types/copy-paste": "^2.1.0", | |
| "@types/debug": "^4.1.12", | |
| "@types/node": "^24.10.10", | |
| "@types/semver": "^7.7.1", | |
| "c12": "^3.3.3", | |
| "citty": "^0.2.0", | |
| "confbox": "^0.2.2", | |
| "consola": "^3.4.2", | |
| "copy-paste": "^2.2.0", | |
| "debug": "^4.4.3", | |
| "defu": "^6.1.4", | |
| "exsolve": "^1.0.8", | |
| "fuse.js": "^7.1.0", | |
| "giget": "^3.1.1", |
🤖 Prompt for AI Agents
In `@packages/nuxi/package.json` at line 54, The package.json currently lists
"fzf" under devDependencies but it is imported/used at runtime in
packages/nuxi/src/commands/module/_autocomplete.ts, so move the "fzf" entry from
devDependencies to dependencies in packages/nuxi/package.json (i.e., remove it
from devDependencies and add the same version under dependencies) and then
reinstall/build so the runtime bundle includes it.
| // If no modules specified, show interactive search | ||
| if (modules.length === 0) { | ||
| const modulesSpinner = spinner() | ||
| modulesSpinner.start('Fetching available modules') | ||
|
|
||
| const [allModules, nuxtVersion] = await Promise.all([ | ||
| fetchModules(), | ||
| getNuxtVersion(cwd), | ||
| ]) | ||
|
|
||
| const compatibleModules = allModules.filter(m => | ||
| !m.compatibility.nuxt || checkNuxtCompatibility(m, nuxtVersion), | ||
| ) | ||
|
|
||
| modulesSpinner.stop('Modules loaded') | ||
|
|
||
| const result = await selectModulesAutocomplete({ | ||
| modules: compatibleModules, | ||
| message: 'Search modules to add (Esc to finish):', | ||
| }) | ||
|
|
||
| if (result.selected.length === 0) { | ||
| cancel('No modules selected.') | ||
| process.exit(0) | ||
| } | ||
|
|
||
| modules = result.selected | ||
| } |
There was a problem hiding this comment.
Handle module fetch failures and explicit cancellation in the interactive flow.
A failed fetch will currently throw and leave the spinner running; also explicit cancellation isn’t handled separately from empty selection. Consider adding a guarded fetch and a result.cancelled branch.
🐛 Suggested hardening
if (modules.length === 0) {
const modulesSpinner = spinner()
modulesSpinner.start('Fetching available modules')
- const [allModules, nuxtVersion] = await Promise.all([
- fetchModules(),
- getNuxtVersion(cwd),
- ])
+ let allModules: NuxtModule[]
+ let nuxtVersion: string
+ try {
+ [allModules, nuxtVersion] = await Promise.all([
+ fetchModules(),
+ getNuxtVersion(cwd),
+ ])
+ }
+ catch (err) {
+ modulesSpinner.stop('Failed to load modules')
+ logger.error(err instanceof Error ? err.message : String(err))
+ process.exit(1)
+ }
const compatibleModules = allModules.filter(m =>
!m.compatibility.nuxt || checkNuxtCompatibility(m, nuxtVersion),
)
modulesSpinner.stop('Modules loaded')
const result = await selectModulesAutocomplete({
modules: compatibleModules,
message: 'Search modules to add (Esc to finish):',
})
+ if (result.cancelled) {
+ cancel('Operation cancelled.')
+ process.exit(1)
+ }
if (result.selected.length === 0) {
cancel('No modules selected.')
process.exit(0)
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| // If no modules specified, show interactive search | |
| if (modules.length === 0) { | |
| const modulesSpinner = spinner() | |
| modulesSpinner.start('Fetching available modules') | |
| const [allModules, nuxtVersion] = await Promise.all([ | |
| fetchModules(), | |
| getNuxtVersion(cwd), | |
| ]) | |
| const compatibleModules = allModules.filter(m => | |
| !m.compatibility.nuxt || checkNuxtCompatibility(m, nuxtVersion), | |
| ) | |
| modulesSpinner.stop('Modules loaded') | |
| const result = await selectModulesAutocomplete({ | |
| modules: compatibleModules, | |
| message: 'Search modules to add (Esc to finish):', | |
| }) | |
| if (result.selected.length === 0) { | |
| cancel('No modules selected.') | |
| process.exit(0) | |
| } | |
| modules = result.selected | |
| } | |
| // If no modules specified, show interactive search | |
| if (modules.length === 0) { | |
| const modulesSpinner = spinner() | |
| modulesSpinner.start('Fetching available modules') | |
| let allModules: NuxtModule[] | |
| let nuxtVersion: string | |
| try { | |
| [allModules, nuxtVersion] = await Promise.all([ | |
| fetchModules(), | |
| getNuxtVersion(cwd), | |
| ]) | |
| } | |
| catch (err) { | |
| modulesSpinner.stop('Failed to load modules') | |
| logger.error(err instanceof Error ? err.message : String(err)) | |
| process.exit(1) | |
| } | |
| const compatibleModules = allModules.filter(m => | |
| !m.compatibility.nuxt || checkNuxtCompatibility(m, nuxtVersion), | |
| ) | |
| modulesSpinner.stop('Modules loaded') | |
| const result = await selectModulesAutocomplete({ | |
| modules: compatibleModules, | |
| message: 'Search modules to add (Esc to finish):', | |
| }) | |
| if (result.cancelled) { | |
| cancel('Operation cancelled.') | |
| process.exit(1) | |
| } | |
| if (result.selected.length === 0) { | |
| cancel('No modules selected.') | |
| process.exit(0) | |
| } | |
| modules = result.selected | |
| } |
🤖 Prompt for AI Agents
In `@packages/nuxi/src/commands/module/add.ts` around lines 88 - 115, The
interactive branch can leave the spinner running on fetch failures and doesn't
handle explicit cancellation from selectModulesAutocomplete; wrap the
Promise.all call that uses fetchModules() and getNuxtVersion(cwd) in a try/catch
(or use .catch) and ensure spinner().stop() is called in a finally block so the
spinner always stops on error, logging or calling cancel() with the caught error
and exiting; after calling selectModulesAutocomplete({modules:
compatibleModules,...}) check result.cancelled first and call cancel('Selection
cancelled.') and exit, then handle result.selected.length === 0 as the
empty-selection case, and keep references to spinner, fetchModules,
getNuxtVersion, selectModulesAutocomplete, cancel and process.exit when
implementing these guards.
Recording.2026-01-11.185725.mp4
Recording.2026-01-11.192052.mp4
Summary
nuxi initfzffor fast fuzzy matchingProbably the UX can be improved, but not sure how, feel free to iterate or suggest improvements 💅
Closes #1174
Test plan
nuxi init /tmp/test